package com.xhj.mybatis.scripting.xmltags;

import com.xhj.mybatis.reflection.MetaObject;
import com.xhj.mybatis.session.Configuration;
import ognl.OgnlContext;
import ognl.OgnlRuntime;
import ognl.PropertyAccessor;

import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;

/**
 * DynamicContext
 *
 * @author XJks
 * @description DynamicContext 公共类，用于构建动态 SQL 上下文（存储绑定变量、拼接 SQL 等）
 */
public class DynamicContext {

    /**
     * 定义一个常量键名，用于在上下文中存放原始传入的参数对象
     */
    public static final String PARAMETER_OBJECT_KEY = "_parameter";

    /**
     * 定义一个常量键名，用于在上下文中存放数据库 id（用于数据库厂商差异化处理）
     */
    public static final String DATABASE_ID_KEY = "_databaseId";

    /**
     * 静态初始化块：在类加载时执行一次，用来为 OGNL 注册自定义的属性访问器
     */
    static {
        // 将 ContextMap 类型对应到自定义的 ContextAccessor，以便 OGNL 在表达式求值时正确访问 map 中的值。
        OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
    }

    /**
     * 使用自定义的 ContextMap 作为绑定变量容器（保存名称->值）
     */
    private final ContextMap bindings;

    /**
     * 用于拼接 SQL 片段，使用空格作为分隔符（保证片段间有空格）
     */
    private final StringJoiner sqlBuilder = new StringJoiner(" ");

    /**
     * 用于生成唯一数字的计数器（getUniqueNumber 每次调用会递增并返回旧值）
     */
    private int uniqueNumber = 0;

    /**
     * 构造函数：接受 MyBatis 的 Configuration 和用户传入的参数对象
     *
     * @param configuration   MyBatis 全局配置对象
     * @param parameterObject 用户传入的参数对象，可以是任意类型或 null
     */
    public DynamicContext(Configuration configuration, Object parameterObject) {
        // 如果传入的参数对象非空且不是 Map，则将其包装成 MetaObject 以便反射访问属性
        if (parameterObject != null && !(parameterObject instanceof Map)) {
            // 通过配置创建 MetaObject，这样可以通过统一接口读写参数对象的属性
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            // 判断参数对象的类是否有对应的 TypeHandler（决定参数是否应当直接作为单一参数回退）
            boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
            // 将 MetaObject 和是否回退的标志传入 ContextMap，用于后续 get 操作时的行为控制
            bindings = new ContextMap(metaObject, existsTypeHandler);
        } else {
            // 如果参数对象为空或已经是 Map，则不需要 MetaObject，直接创建不回退的 ContextMap
            bindings = new ContextMap(null, false);
        }
        // 将原始参数对象放入 bindings，key 为 PARAMETER_OBJECT_KEY，便于 OGNL 或其他代码访问原始对象
        bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
        // 将数据库 id 放入 bindings，key 为 DATABASE_ID_KEY，便于动态 SQL 根据数据库差异进行选择
        bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
    }

    /**
     * 暴露出 bindings，返回一个 Map<String, Object> 视图，外部可以读取已绑定的变量
     *
     * @return 绑定变量的 Map 视图
     */
    public Map<String, Object> getBindings() {
        return bindings;
    }

    /**
     * 在上下文中绑定一个名字和值
     *
     * @param name  绑定变量的名称
     * @param value 绑定变量的值
     */
    public void bind(String name, Object value) {
        bindings.put(name, value);
    }

    /**
     * 将一段 SQL 片段追加到 sqlBuilder 中（片段之间用空格隔开）
     *
     * @param sql 要追加的 SQL 片段
     */
    public void appendSql(String sql) {
        sqlBuilder.add(sql);
    }

    /**
     * 返回当前拼接好的 SQL 字符串，去除首尾多余空白
     *
     * @return 拼接好的 SQL 字符串
     */
    public String getSql() {
        return sqlBuilder.toString().trim();
    }

    /**
     * 返回一个唯一数字（先返回当前值，然后自增），常用于生成占位符或变量名
     *
     * @return 唯一数字
     */
    public int getUniqueNumber() {
        return uniqueNumber++;
    }

    /**
     * ContextMap 是 DynamicContext 内部使用的 Map 实现，带有对参数对象的快捷读取能力。
     * TODO:
     *  该类的主要作用是对 MappedStatement 中传入的参数进行统一封装，以便 OGNL 在动态 SQL 拼接过程中能够方便地访问参数值。
     *  把传入的参数统一用 ContextMap（继承自 HashMap）包起来后，OGNL 在动态拼接 SQL 时不会直接操作原始对象，而是通过事先注册的 ContextAccessor 按照 Map 的访问规则去读取 ContextMap 的值。
     *  ContextMap 内部可能直接保存一个 Map，也可能封装一个通过 MetaObject 包装的 POJO，但对外统一以 Map 的键值方式访问，这样动态表达式既能访问 Map 的键也能访问 POJO 的属性，调用方无需关心底层类型。
     */
    static class ContextMap extends HashMap<String, Object> {
        /**
         * 序列化 UID（虽然通常不需要，但能避免警告）
         */
        private static final long serialVersionUID = 1L;

        /**
         * 如果有非 Map 参数对象，则保存其 MetaObject，以便基于属性名取值
         */
        private final MetaObject parameterMetaObject;

        /**
         * 如果为 true，表示在某些情况下可以回退到整个参数对象（而不是单个属性）
         */
        private final boolean fallbackParameterObject;

        /**
         * 构造函数：传入参数对象对应的 MetaObject 以及是否允许回退到整个参数对象
         *
         * @param parameterMetaObject     参数对象的 MetaObject
         * @param fallbackParameterObject 是否允许回退到整个参数对象
         */
        public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) {
            this.parameterMetaObject = parameterMetaObject;
            this.fallbackParameterObject = fallbackParameterObject;
        }

        @Override
        public Object get(Object key) {
            // 1.尝试从父类 Map 中获取值
            // 将 key 强转为字符串（上下文的键都是字符串）
            String strKey = (String) key;
            // 如果父类 Map 中已经包含该 key（例如通过 bind 放入的），则直接返回父类的值
            if (super.containsKey(strKey)) {
                return super.get(strKey);
            }

            // 2.尝试从 parameterMetaObject 中获取对应属性的值
            // 如果没有 parameterMetaObject（没有非 Map 的参数对象），则无法从参数对象中读取，返回 null
            if (parameterMetaObject == null) {
                return null;
            }

            // 3.根据两种情况处理
            if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
                // 3.1 如果开启了 fallback（表示参数不是 Map 且有对应 TypeHandler），并且参数对象没有该属性的 getter，则返回整个参数对象
                return parameterMetaObject.getOriginalObject();
            } else {
                // 3.2 如果没有开启 fallback，或者参数对象有该属性的 getter，则正常通过属性名获取对应值，即通过 MetaObject 去读取对应属性的值
                return parameterMetaObject.getValue(strKey);
            }
        }
    }

    /**
     * ContextAccessor 是为了让 OGNL 在解析表达式访问 ContextMap 时使用的自定义访问器
     */
    static class ContextAccessor implements PropertyAccessor {

        /**
         * 从 target（应当是一个 Map）中根据 name 取值，OGNL 在表达式求值时会调用此方法。
         *
         * @param context 当前执行上下文
         * @param target  要从中获取属性的对象
         * @param name    要获取的属性的名称
         * @return 给定对象中指定属性的当前值
         */
        @Override
        public Object getProperty(Map context, Object target, Object name) {
            // 将目标对象强转为 Map（ContextMap 继承自 HashMap）
            Map map = (Map) target;

            // 先尝试直接从 Map 中取值
            Object result = map.get(name);
            // 如果 Map 中包含这个键（即使值为 null）或取到的结果不为 null，则直接返回 result
            if (map.containsKey(name) || result != null) {
                return result;
            }

            // 若 Map 中没有该键，尝试从存放的原始参数对象中取值（key 为 PARAMETER_OBJECT_KEY）
            Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
            // 如果原始参数对象本身是一个 Map，则从中取相同的 key 值并返回
            if (parameterObject instanceof Map) {
                return ((Map) parameterObject).get(name);
            }

            // 其他情况返回 null（表示未找到该属性）
            return null;
        }

        /**
         * 设置属性：把 name->value 放到 target Map 中（OGNL 在赋值表达式时会调用）
         *
         * @param context 当前执行上下文
         * @param target  要设置属性的对象
         * @param name    要设置的属性的名称
         * @param value   要设置的属性值
         */
        @Override
        public void setProperty(Map context, Object target, Object name, Object value) {
            // 这里把 target 强转为通用 Map 并放入键值
            Map<Object, Object> map = (Map<Object, Object>) target;
            map.put(name, value);
        }

        /**
         * 返回一个 Java 字符串，表示应调用的用于访问特定元素的文本方法名（例如 "get"）。
         * <p>
         * TODO:
         *  {@link #getSourceAccessor} 和 {@link #getSourceSetter}这两个方法来自 {@link ognl.PropertyAccessor} 接口。
         *  用于在 OGNL 将表达式“编译”为 Java 源代码时，提供用于读取（getSourceAccessor）或写入（getSourceSetter）属性的 Java 源片段。
         *  返回非空字符串时，OGNL 会把该源码片段直接拼入生成的访问器以获得更好的性能。
         *  返回 null 表示不提供源码片段，OGNL 将退回到运行时反射或默认访问路径。
         *  参数含义：第一个是 OgnlContext（上下文），后两个是目标对象和属性名，用来生成针对该目标/属性的源码。
         *  在 MyBatis 的场景下通常不需要 OGNL 的源码生成，返回 null 即表示禁用该优化，行为是安全且预期的。
         *
         * @param context 当前执行的上下文。
         * @param target  在被求值的表达式树中当前的目标对象。
         * @param index   将被放入字符串中以访问值的索引对象。
         * @return 要调用的源访问器方法名。
         */
        @Override
        public String getSourceAccessor(OgnlContext context, Object target, Object index) {
            return null;
        }

        /**
         * 返回一个 Java 字符串，表示应调用的用于设置特定元素的文本方法名（例如 "set"）。
         *
         * @param context 当前的执行上下文。
         * @param target  在表达式树中正在被求值的当前对象目标。
         * @param index   将被放入字符串中以设置值的索引对象。
         * @return 要调用的源设置器方法名。
         */
        @Override
        public String getSourceSetter(OgnlContext context, Object target, Object index) {
            return null;
        }
    }
}
